EarCut.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. /**
  2. * 此为基于开源项目 https://github.com/mapbox/earcut.git 翻译的typescript版本
  3. * author:soida
  4. * mail:flashfin@foxmail.com
  5. * The open source license is consistent with the original earcut.js project
  6. */
  7. export interface EarcutNode {
  8. i: number;
  9. x: number;
  10. y: number;
  11. prev: EarcutNode;
  12. next: EarcutNode;
  13. z: number;
  14. prevZ: EarcutNode | null;
  15. nextZ: EarcutNode | null;
  16. steiner: boolean;
  17. }
  18. /**
  19. *
  20. * @param data [x1,y1,z1,...]或者[x1,y1,x2,y2,...]
  21. * @param holeIndices 中间有洞,此为洞的点起始索引,比如第几个点开始是洞,如果没有洞,此参数可以不传
  22. * @param dim 数据的维度,比如[x,y,z]为3维数据,此时dim=3,如果是[x,y]为2维数据,此时dim=2
  23. * @returns 返回可以直接用于渲染的索引
  24. * 对于输入数据,需要预先处理自相交和连续共线点,否则可能输出索引渲染结果不符合预期
  25. * 建议外环数据逆时针,内环数据顺时针,虽然earcut有一定自动处理顺序能力,但某些情况下依然会出现渲染不符预期
  26. */
  27. export default function earcut(data: number[], holeIndices?: number[], dim: number = 2): number[] {
  28. const hasHoles = holeIndices && holeIndices.length;
  29. const outerLen = hasHoles ? holeIndices![0] * dim : data.length;
  30. let outerNode = linkedList(data, 0, outerLen, dim, true);
  31. const triangles: number[] = [];
  32. if (!outerNode || outerNode.next === outerNode.prev) return triangles;
  33. let minX: number, minY: number, invSize: number;
  34. if (hasHoles) outerNode = eliminateHoles(data, holeIndices!, outerNode, dim);
  35. if (data.length > 80 * dim) {
  36. minX = Infinity;
  37. minY = Infinity;
  38. let maxX = -Infinity;
  39. let maxY = -Infinity;
  40. for (let i = dim; i < outerLen; i += dim) {
  41. const x = data[i];
  42. const y = data[i + 1];
  43. if (x < minX) minX = x;
  44. if (y < minY) minY = y;
  45. if (x > maxX) maxX = x;
  46. if (y > maxY) maxY = y;
  47. }
  48. invSize = Math.max(maxX - minX, maxY - minY);
  49. invSize = invSize !== 0 ? 32767 / invSize : 0;
  50. }
  51. earcutLinked(outerNode, triangles, dim, minX!, minY!, invSize!, 0);
  52. return triangles;
  53. }
  54. function linkedList(data: number[], start: number, end: number, dim: number, clockwise: boolean): EarcutNode | null {
  55. let last: EarcutNode | undefined;
  56. if (clockwise === signedArea(data, start, end, dim) > 0) {
  57. for (let i = start; i < end; i += dim) last = insertNode((i / dim) | 0, data[i], data[i + 1], last);
  58. } else {
  59. for (let i = end - dim; i >= start; i -= dim) last = insertNode((i / dim) | 0, data[i], data[i + 1], last);
  60. }
  61. if (last && equals(last, last.next)) {
  62. removeNode(last);
  63. last = last.next;
  64. }
  65. return last || null;
  66. }
  67. function filterPoints(start: EarcutNode | null, end?: EarcutNode | null): EarcutNode | null {
  68. if (!start) return start;
  69. if (!end) end = start;
  70. let p = start,
  71. again: boolean;
  72. do {
  73. again = false;
  74. if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
  75. removeNode(p);
  76. p = end = p.prev;
  77. if (p === p.next) break;
  78. again = true;
  79. } else {
  80. p = p.next;
  81. }
  82. } while (again || p !== end);
  83. return end;
  84. }
  85. function earcutLinked(
  86. ear: EarcutNode | null,
  87. triangles: number[],
  88. dim: number,
  89. minX: number,
  90. minY: number,
  91. invSize: number,
  92. pass: number
  93. ): void {
  94. if (!ear) return;
  95. if (!pass && invSize) indexCurve(ear, minX, minY, invSize);
  96. let stop = ear;
  97. while (ear.prev !== ear.next) {
  98. const prev = ear.prev;
  99. const next = ear.next;
  100. if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
  101. triangles.push(prev.i, ear.i, next.i);
  102. removeNode(ear);
  103. ear = next.next;
  104. stop = next.next;
  105. continue;
  106. }
  107. ear = next;
  108. if (ear === stop) {
  109. if (!pass) {
  110. earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);
  111. } else if (pass === 1) {
  112. ear = cureLocalIntersections(filterPoints(ear), triangles);
  113. earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);
  114. } else if (pass === 2) {
  115. splitEarcut(ear, triangles, dim, minX, minY, invSize);
  116. }
  117. break;
  118. }
  119. }
  120. }
  121. function isEar(ear: EarcutNode): boolean {
  122. const a = ear.prev,
  123. b = ear,
  124. c = ear.next;
  125. if (area(a, b, c) >= 0) return false;
  126. const ax = a.x,
  127. bx = b.x,
  128. cx = c.x,
  129. ay = a.y,
  130. by = b.y,
  131. cy = c.y;
  132. const x0 = Math.min(ax, bx, cx),
  133. y0 = Math.min(ay, by, cy),
  134. x1 = Math.max(ax, bx, cx),
  135. y1 = Math.max(ay, by, cy);
  136. let p = c.next;
  137. while (p !== a) {
  138. if (
  139. p.x >= x0 &&
  140. p.x <= x1 &&
  141. p.y >= y0 &&
  142. p.y <= y1 &&
  143. pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
  144. area(p.prev, p, p.next) >= 0
  145. )
  146. return false;
  147. p = p.next;
  148. }
  149. return true;
  150. }
  151. function isEarHashed(ear: EarcutNode, minX: number, minY: number, invSize: number): boolean {
  152. const a = ear.prev,
  153. b = ear,
  154. c = ear.next;
  155. if (area(a, b, c) >= 0) return false;
  156. const ax = a.x,
  157. bx = b.x,
  158. cx = c.x,
  159. ay = a.y,
  160. by = b.y,
  161. cy = c.y;
  162. const x0 = Math.min(ax, bx, cx),
  163. y0 = Math.min(ay, by, cy),
  164. x1 = Math.max(ax, bx, cx),
  165. y1 = Math.max(ay, by, cy);
  166. const minZ = zOrder(x0, y0, minX, minY, invSize),
  167. maxZ = zOrder(x1, y1, minX, minY, invSize);
  168. let p = ear.prevZ,
  169. n = ear.nextZ;
  170. while (p && p.z >= minZ && n && n.z <= maxZ) {
  171. if (
  172. p.x >= x0 &&
  173. p.x <= x1 &&
  174. p.y >= y0 &&
  175. p.y <= y1 &&
  176. p !== a &&
  177. p !== c &&
  178. pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
  179. area(p.prev, p, p.next) >= 0
  180. )
  181. return false;
  182. p = p.prevZ;
  183. if (
  184. n.x >= x0 &&
  185. n.x <= x1 &&
  186. n.y >= y0 &&
  187. n.y <= y1 &&
  188. n !== a &&
  189. n !== c &&
  190. pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) &&
  191. area(n.prev, n, n.next) >= 0
  192. )
  193. return false;
  194. n = n.nextZ;
  195. }
  196. while (p && p.z >= minZ) {
  197. if (
  198. p.x >= x0 &&
  199. p.x <= x1 &&
  200. p.y >= y0 &&
  201. p.y <= y1 &&
  202. p !== a &&
  203. p !== c &&
  204. pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
  205. area(p.prev, p, p.next) >= 0
  206. )
  207. return false;
  208. p = p.prevZ;
  209. }
  210. while (n && n.z <= maxZ) {
  211. if (
  212. n.x >= x0 &&
  213. n.x <= x1 &&
  214. n.y >= y0 &&
  215. n.y <= y1 &&
  216. n !== a &&
  217. n !== c &&
  218. pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) &&
  219. area(n.prev, n, n.next) >= 0
  220. )
  221. return false;
  222. n = n.nextZ;
  223. }
  224. return true;
  225. }
  226. function cureLocalIntersections(start: EarcutNode, triangles: number[]): EarcutNode {
  227. let p = start;
  228. do {
  229. const a = p.prev,
  230. b = p.next.next;
  231. if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
  232. triangles.push(a.i, p.i, b.i);
  233. removeNode(p);
  234. removeNode(p.next);
  235. p = start = b;
  236. }
  237. p = p.next;
  238. } while (p !== start);
  239. return filterPoints(p)!;
  240. }
  241. function splitEarcut(
  242. start: EarcutNode,
  243. triangles: number[],
  244. dim: number,
  245. minX: number,
  246. minY: number,
  247. invSize: number
  248. ): void {
  249. let a = start;
  250. do {
  251. let b = a.next.next;
  252. while (b !== a.prev) {
  253. if (a.i !== b.i && isValidDiagonal(a, b)) {
  254. let c = splitPolygon(a, b);
  255. a = filterPoints(a, a.next)!;
  256. c = filterPoints(c, c.next)!;
  257. earcutLinked(a, triangles, dim, minX, minY, invSize, 0);
  258. earcutLinked(c, triangles, dim, minX, minY, invSize, 0);
  259. return;
  260. }
  261. b = b.next;
  262. }
  263. a = a.next;
  264. } while (a !== start);
  265. }
  266. function eliminateHoles(data: number[], holeIndices: number[], outerNode: EarcutNode, dim: number): EarcutNode {
  267. const queue: EarcutNode[] = [];
  268. for (let i = 0, len = holeIndices.length; i < len; i++) {
  269. const start = holeIndices[i] * dim;
  270. const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
  271. const list = linkedList(data, start, end, dim, false)!;
  272. if (list === list.next) list.steiner = true;
  273. queue.push(getLeftmost(list));
  274. }
  275. queue.sort(compareXYSlope);
  276. for (let i = 0; i < queue.length; i++) {
  277. outerNode = eliminateHole(queue[i], outerNode);
  278. }
  279. return outerNode;
  280. }
  281. function compareXYSlope(a: EarcutNode, b: EarcutNode): number {
  282. let result = a.x - b.x;
  283. if (result === 0) {
  284. result = a.y - b.y;
  285. if (result === 0) {
  286. const aSlope = (a.next.y - a.y) / (a.next.x - a.x);
  287. const bSlope = (b.next.y - b.y) / (b.next.x - b.x);
  288. result = aSlope - bSlope;
  289. }
  290. }
  291. return result;
  292. }
  293. function eliminateHole(hole: EarcutNode, outerNode: EarcutNode): EarcutNode {
  294. const bridge = findHoleBridge(hole, outerNode);
  295. if (!bridge) return outerNode;
  296. const bridgeReverse = splitPolygon(bridge, hole);
  297. filterPoints(bridgeReverse, bridgeReverse.next);
  298. return filterPoints(bridge, bridge.next)!;
  299. }
  300. function findHoleBridge(hole: EarcutNode, outerNode: EarcutNode): EarcutNode | null {
  301. let p = outerNode;
  302. const hx = hole.x;
  303. const hy = hole.y;
  304. let qx = -Infinity;
  305. let m: EarcutNode | undefined;
  306. if (equals(hole, p)) return p;
  307. do {
  308. if (equals(hole, p.next)) return p.next;
  309. else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
  310. const x = p.x + ((hy - p.y) * (p.next.x - p.x)) / (p.next.y - p.y);
  311. if (x <= hx && x > qx) {
  312. qx = x;
  313. m = p.x < p.next.x ? p : p.next;
  314. if (x === hx) return m;
  315. }
  316. }
  317. p = p.next;
  318. } while (p !== outerNode);
  319. if (!m) return null;
  320. const stop = m;
  321. const mx = m.x;
  322. const my = m.y;
  323. let tanMin = Infinity;
  324. p = m;
  325. do {
  326. if (
  327. hx >= p.x &&
  328. p.x >= mx &&
  329. hx !== p.x &&
  330. pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)
  331. ) {
  332. const tan = Math.abs(hy - p.y) / (hx - p.x);
  333. if (
  334. locallyInside(p, hole) &&
  335. (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))
  336. ) {
  337. m = p;
  338. tanMin = tan;
  339. }
  340. }
  341. p = p.next;
  342. } while (p !== stop);
  343. return m;
  344. }
  345. function sectorContainsSector(m: EarcutNode, p: EarcutNode): boolean {
  346. return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
  347. }
  348. function indexCurve(start: EarcutNode, minX: number, minY: number, invSize: number): void {
  349. let p = start;
  350. do {
  351. if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
  352. p.prevZ = p.prev;
  353. p.nextZ = p.next;
  354. p = p.next;
  355. } while (p !== start);
  356. p.prevZ!.nextZ = null;
  357. p.prevZ = null;
  358. sortLinked(p);
  359. }
  360. function sortLinked(list: EarcutNode): EarcutNode {
  361. let numMerges: number;
  362. let inSize = 1;
  363. do {
  364. let p: EarcutNode | null = list;
  365. let e: EarcutNode | null;
  366. list = null as any;
  367. let tail: EarcutNode | null = null;
  368. numMerges = 0;
  369. while (p) {
  370. numMerges++;
  371. let q = p;
  372. let pSize = 0;
  373. for (let i = 0; i < inSize; i++) {
  374. pSize++;
  375. q = q.nextZ!;
  376. if (!q) break;
  377. }
  378. let qSize = inSize;
  379. while (pSize > 0 || (qSize > 0 && q)) {
  380. if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
  381. e = p;
  382. p = p.nextZ!;
  383. pSize--;
  384. } else {
  385. e = q;
  386. q = q.nextZ!;
  387. qSize--;
  388. }
  389. if (tail) tail.nextZ = e;
  390. else list = e;
  391. e.prevZ = tail;
  392. tail = e;
  393. }
  394. p = q;
  395. }
  396. tail!.nextZ = null;
  397. inSize *= 2;
  398. } while (numMerges > 1);
  399. return list;
  400. }
  401. function zOrder(x: number, y: number, minX: number, minY: number, invSize: number): number {
  402. x = ((x - minX) * invSize) | 0;
  403. y = ((y - minY) * invSize) | 0;
  404. x = (x | (x << 8)) & 0x00ff00ff;
  405. x = (x | (x << 4)) & 0x0f0f0f0f;
  406. x = (x | (x << 2)) & 0x33333333;
  407. x = (x | (x << 1)) & 0x55555555;
  408. y = (y | (y << 8)) & 0x00ff00ff;
  409. y = (y | (y << 4)) & 0x0f0f0f0f;
  410. y = (y | (y << 2)) & 0x33333333;
  411. y = (y | (y << 1)) & 0x55555555;
  412. return x | (y << 1);
  413. }
  414. function getLeftmost(start: EarcutNode): EarcutNode {
  415. let p = start,
  416. leftmost = start;
  417. do {
  418. if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;
  419. p = p.next;
  420. } while (p !== start);
  421. return leftmost;
  422. }
  423. function pointInTriangle(
  424. ax: number,
  425. ay: number,
  426. bx: number,
  427. by: number,
  428. cx: number,
  429. cy: number,
  430. px: number,
  431. py: number
  432. ): boolean {
  433. return (
  434. (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&
  435. (ax - px) * (by - py) >= (bx - px) * (ay - py) &&
  436. (bx - px) * (cy - py) >= (cx - px) * (by - py)
  437. );
  438. }
  439. function pointInTriangleExceptFirst(
  440. ax: number,
  441. ay: number,
  442. bx: number,
  443. by: number,
  444. cx: number,
  445. cy: number,
  446. px: number,
  447. py: number
  448. ): boolean {
  449. return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
  450. }
  451. function isValidDiagonal(a: EarcutNode, b: EarcutNode): boolean {
  452. return (
  453. a.next.i !== b.i &&
  454. a.prev.i !== b.i &&
  455. !intersectsPolygon(a, b) &&
  456. ((locallyInside(a, b) &&
  457. locallyInside(b, a) &&
  458. middleInside(a, b) &&
  459. (area(a.prev, a, b.prev) !== 0 || area(a, b.prev, b) !== 0)) ||
  460. (equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0))
  461. );
  462. }
  463. function area(p: EarcutNode, q: EarcutNode, r: EarcutNode): number {
  464. return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
  465. }
  466. function equals(p1: EarcutNode, p2: EarcutNode): boolean {
  467. return p1.x === p2.x && p1.y === p2.y;
  468. }
  469. function intersects(p1: EarcutNode, q1: EarcutNode, p2: EarcutNode, q2: EarcutNode): boolean {
  470. const o1 = sign(area(p1, q1, p2));
  471. const o2 = sign(area(p1, q1, q2));
  472. const o3 = sign(area(p2, q2, p1));
  473. const o4 = sign(area(p2, q2, q1));
  474. if (o1 !== o2 && o3 !== o4) return true;
  475. if (o1 === 0 && onSegment(p1, p2, q1)) return true;
  476. if (o2 === 0 && onSegment(p1, q2, q1)) return true;
  477. if (o3 === 0 && onSegment(p2, p1, q2)) return true;
  478. if (o4 === 0 && onSegment(p2, q1, q2)) return true;
  479. return false;
  480. }
  481. function onSegment(p: EarcutNode, q: EarcutNode, r: EarcutNode): boolean {
  482. return (
  483. q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y)
  484. );
  485. }
  486. function sign(num: number): number {
  487. return num > 0 ? 1 : num < 0 ? -1 : 0;
  488. }
  489. function intersectsPolygon(a: EarcutNode, b: EarcutNode): boolean {
  490. let p = a;
  491. do {
  492. if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i && intersects(p, p.next, a, b)) return true;
  493. p = p.next;
  494. } while (p !== a);
  495. return false;
  496. }
  497. function locallyInside(a: EarcutNode, b: EarcutNode): boolean {
  498. return area(a.prev, a, a.next) < 0
  499. ? area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0
  500. : area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
  501. }
  502. function middleInside(a: EarcutNode, b: EarcutNode): boolean {
  503. let p = a;
  504. let inside = false;
  505. const px = (a.x + b.x) / 2;
  506. const py = (a.y + b.y) / 2;
  507. do {
  508. if (p.y > py !== p.next.y > py && p.next.y !== p.y && px < ((p.next.x - p.x) * (py - p.y)) / (p.next.y - p.y) + p.x)
  509. inside = !inside;
  510. p = p.next;
  511. } while (p !== a);
  512. return inside;
  513. }
  514. function splitPolygon(a: EarcutNode, b: EarcutNode): EarcutNode {
  515. const a2 = createNode(a.i, a.x, a.y);
  516. const b2 = createNode(b.i, b.x, b.y);
  517. const an = a.next;
  518. const bp = b.prev;
  519. a.next = b;
  520. b.prev = a;
  521. a2.next = an;
  522. an.prev = a2;
  523. b2.next = a2;
  524. a2.prev = b2;
  525. bp.next = b2;
  526. b2.prev = bp;
  527. return b2;
  528. }
  529. function insertNode(i: number, x: number, y: number, last?: EarcutNode): EarcutNode {
  530. const p = createNode(i, x, y);
  531. if (!last) {
  532. p.prev = p;
  533. p.next = p;
  534. } else {
  535. p.next = last.next;
  536. p.prev = last;
  537. last.next.prev = p;
  538. last.next = p;
  539. }
  540. return p;
  541. }
  542. function removeNode(p: EarcutNode): void {
  543. p.next.prev = p.prev;
  544. p.prev.next = p.next;
  545. if (p.prevZ) p.prevZ.nextZ = p.nextZ;
  546. if (p.nextZ) p.nextZ.prevZ = p.prevZ;
  547. }
  548. function createNode(i: number, x: number, y: number): EarcutNode {
  549. // @ts-ignore
  550. return {
  551. i,
  552. x,
  553. y,
  554. prev: null!,
  555. next: null!,
  556. z: 0,
  557. prevZ: null,
  558. nextZ: null,
  559. steiner: false,
  560. };
  561. }
  562. export function deviation(data: number[], holeIndices: number[] | undefined, dim: number, triangles: number[]): number {
  563. const hasHoles = holeIndices && holeIndices.length;
  564. const outerLen = hasHoles ? holeIndices![0] * dim : data.length;
  565. let polygonArea = Math.abs(signedArea(data, 0, outerLen, dim));
  566. if (hasHoles) {
  567. for (let i = 0, len = holeIndices!.length; i < len; i++) {
  568. const start = holeIndices![i] * dim;
  569. const end = i < len - 1 ? holeIndices![i + 1] * dim : data.length;
  570. polygonArea -= Math.abs(signedArea(data, start, end, dim));
  571. }
  572. }
  573. let trianglesArea = 0;
  574. for (let i = 0; i < triangles.length; i += 3) {
  575. const a = triangles[i] * dim;
  576. const b = triangles[i + 1] * dim;
  577. const c = triangles[i + 2] * dim;
  578. trianglesArea += Math.abs(
  579. (data[a] - data[c]) * (data[b + 1] - data[a + 1]) - (data[a] - data[b]) * (data[c + 1] - data[a + 1])
  580. );
  581. }
  582. return polygonArea === 0 && trianglesArea === 0 ? 0 : Math.abs((trianglesArea - polygonArea) / polygonArea);
  583. }
  584. function signedArea(data: number[], start: number, end: number, dim: number): number {
  585. let sum = 0;
  586. for (let i = start, j = end - dim; i < end; i += dim) {
  587. sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
  588. j = i;
  589. }
  590. return sum;
  591. }
  592. export function flatten(data: number[][][]): { vertices: number[]; holes: number[]; dimensions: number } {
  593. const vertices: number[] = [];
  594. const holes: number[] = [];
  595. const dimensions = data[0][0].length;
  596. let holeIndex = 0;
  597. let prevLen = 0;
  598. for (const ring of data) {
  599. for (const p of ring) {
  600. for (let d = 0; d < dimensions; d++) vertices.push(p[d]);
  601. }
  602. if (prevLen) {
  603. holeIndex += prevLen;
  604. holes.push(holeIndex);
  605. }
  606. prevLen = ring.length;
  607. }
  608. return { vertices, holes, dimensions };
  609. }